<?php

/*
 * Copyright 2008 the original author or authors.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#namespace tehframework\iocc;

class MutableContainer extends Container
{
	/**
	 * @var Container?
	 */
	protected $parentContainer;
	
	/**
	 * @var Storage
	 */
	protected $bindings;
	
	/**
	 * @var array<string,Storage>
	 */
	private $scopes = array();
	
	/**
	 * @var bool
	 */
	private $autowiring = false;
	
	/**
	 * @var ObjectBuilder
	 */
	private $objectBuilder;
	
	/**
	 * @param  Storage? $bindings
	 * @param  Container? $parentContainer
	 */
	public function __construct(Storage $bindings = null, Container $parentContainer = null)
	{
		$this->bindings        = $bindings ? $bindings : new TransientStorage();
		$this->parentContainer = $parentContainer;
	}
	
	/**
	 * @param  string $type
	 * @param  string? $name
	 * @return bool
	 */
	public function isResolvable($type, $name = null)
	{
		$key = $this->genBindingKey($type, $name);
		
		return $this->containsBinding($type, $name)
			|| $this->isResolvableByParentContainer($type, $name)
			|| $this->isResolvableByAutowiring($type, $name);
	}
	
	/**
	 * @param  string $type
	 * @param  string? $name
	 * @param  array<int,mixed>? $args
	 * @return object
	 * @throws ObjectBuildingException
	 */
	public function resolve($type, $name = null, array $args = null)
	{
		if (!$this->containsBinding($type, $name))
		{
			if ($this->isResolvableByParentContainer($type, $name))
			{
				return $this->parentContainer->resolve($type, $name, $args);
			}
			
			if (!$this->isResolvableByAutowiring($type, $name))
			{
				throw new ObjectBuildingException(sprintf(
					'Can not resolve [%s] object. Autowiring was not possible.', $type
				));
			}
			
			$binding = new AutowiredBinding($type, $name);
			
			$this->registerBinding($binding);
		}
		else
		{
			$binding = $this->bindings->restore($this->genBindingKey($type, $name));
		}
		
		return $this->resolveBinding($binding, $args);
	}
	
	/**
	 * @param  string $type
	 * @param  string? $name
	 * @return bool
	 */
	public function containsBinding($type, $name = null)
	{
		return $this->bindings->restore($this->genBindingKey($type, $name)) !== null;
	}
	
	/**
	 * @param  Binding $binding
	 * @param  string? $name
	 * @return MutableContainer
	 */
	public function registerBinding(Binding $binding)
	{
		$name = $binding->getName();
		
		foreach ($binding->getTypes() as $type)
		{
			$this->bindings->store($this->genBindingKey($type, $name), $binding);
		}
		
		return $this;
	}
	
	/**
	 * @param  string $scope
	 * @param  Storage $storage
	 * @return MutableContainer
	 */
	public function registerScope($scope, Storage $storage)
	{
		$this->scopes[$scope] = $storage;
		
		return $this;
	}
	
	/**
	 * @param  ObjectBuilder $builder
	 * @return MutableContainer
	 */
	public function setBuilder(ObjectBuilder $builder)
	{
		$this->builder = $builder;
		
		return $this;
	}
	
	/**
	 * @return MutableContainer
	 */
	public function enableAutowiring()
	{
		$this->autowiring = true;
		
		return $this;
	}

	/**
	 * @return MutableContainer
	 */
	public function disableAutowiring()
	{
		$this->autowiring = false;
		
		return $this;
	}
	
	/**
	 * @param  string $type
	 * @param  string? $name
	 * @return string
	 */
	protected function genBindingKey($type, $name)
	{
		return (string)crc32($type . $name);
	}
	
	/**
	 * @param  Binding $binding
	 * @param  array<int,mixed>? $args
	 * @return object
	 * @throws ObjectBuildingException
	 */
	private function resolveBinding(Binding $binding, array $args = null)
	{
		$scope = $binding->getScope();
		
		if (empty($scope))
		{
			return $this->buildObject($binding, $args);
		}
		
		if (!isset($this->scopes[$scope]))
		{
			throw new ObjectBuildingException(sprintf('Use of unregistered scope [%s].', $scope));
		}
		
		$key    = $binding->getKey();
		$object = $this->scopes[$scope]->restore($key);
		
		if ($object === null)
		{
			$object = $this->buildObject($binding, $args);
			
			$this->scopes[$scope]->store($key, $object);
		}
		
		return $object;
	}
	
	/**
	 * @param  Binding $binding
	 * @param  array<int,mixed>? $args
	 * @return object
	 * @throws ObjectBuildingException
	 */
	private function buildObject(Binding $binding, array $args = null)
	{
		$provider = $binding->getProvider();
		
		if ($provider !== null)
		{
			return call_user_func($provider, $this, $args);
		}
		
		if ($this->objectBuilder === null)
		{
			$this->objectBuilder = new ObjectBuilder();
		}
		
		return $this->objectBuilder->buildObject($binding->getDefinition(), $this, $args);
	}
	
	/**
	 * @param  string $type
	 * @param  string? $name
	 * @return bool
	 */
	private function isResolvableByParentContainer($type, $name)
	{
		return $this->parentContainer && $this->parentContainer->isResolvable($type, $name);
	}
	
	/**
	 * @param  string $type
	 * @param  string? $name
	 * @return bool
	 */
	private function isResolvableByAutowiring($type, $name)
	{
		return ($name === null) && ($this->autowiring ? class_exists($type) : false);
	}
}